<?php
/**
 * Copyright (c) 2006-2008, Julien PORTALIER
 * http://featherphp.googlecode.com/
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 */

if (!defined('LF')) define('LF', "\n");
if (!defined('CR')) define('CR', "\r");
define('PO_QUOTE', '"');

/// Manages gettext PO files.
class Gettext_PO
{
	private $filename;
	private $header;
	private $strings = array();
	
	/**
	 * 	 * @param $filename String[Optional]
	 */
	function __construct($filename=null)
	{
		if ($filename !== null)
		{
			$this->read($filename);
		}
	}
	
	/**
	 * Parses a PO file.
	 * 
	 * @param $filename String Path to the PO file to parse.
	 */
	function read($filename)
	{
		if (!file_exists($filename))
			throw new Exception("No such file: $filename", 0);
		
		$this->filename = $filename;
		
		// load messages
		$msgs = file_get_contents($this->filename);
		$msgs = explode(LF.LF, str_replace(CR, '', $msgs));
		
		// extract header
		$this->header = array_shift($msgs);
		
		// parse messages
		foreach($msgs as $msg)
		{
			if (empty($msg))
				continue;
			
			$lines = explode(LF, $msg);
			$msg   = array(
				'fuzzy'  => false,
				'#'      => array(),
				'msgid'  => '',
				'msgstr' => ''
			);
			
			for ($i = 0; $i < count($lines); $i++)
			{
				$line = trim($lines[$i]);
				
				# comments + informations
				if (strpos($line, '#') === 0)
				{
					if (preg_match('/#,(\s+)fuzzy/', $line)) {
						$msg['fuzzy'] = true;
					}
					$msg['#'][] = $line;
				}
				
				# msg: plural form
				else if (strpos($line, 'msgid_plural') === 0)
				{
					if (strpos($line, 'msgid_plural ""') === 0)
					{
						# multiline
						$line = array();
						while (isset($lines[$i+1]) and !strpos($lines[$i+1], 'msgstr') === 0)
						{
							$str = $lines[++$i];
							$line[] = substr($str, 1, strlen($str) - 2);
						}
						$line = implode(' ', $line);
					}
					else
					{
						$line = preg_replace('/^msgid_plural /',  '', $line);
						$line = substr($line, 1, strlen($line) - 2);
					}
					$msg['msgid_plural'] = $this->__unescape($line);
				}
				
				# msg: singular form
				else if (strpos($line, 'msgid') === 0)
				{
					if (strpos($line, 'msgid ""') === 0)
					{
						# multiline
						$line = array();
						while (isset($lines[$i+1]) and !preg_match('/^(msgstr|msgid_plural) /', $lines[$i+1]))
						{
							$str = $lines[++$i];
							$line[] = substr($str, 1, strlen($str) - 2);
						}
						$line = implode(' ', $line);
					}
					else
					{
						$line = preg_replace('/^msgid /',  '', $line);
						$line = substr($line, 1, strlen($line) - 2);
					}
					
					$msg['msgid'] = $this->__unescape($line);
				}
				
				# msg: translation(s)
				else if (strpos($line, 'msgstr') === 0)
				{
					if (strpos($line, 'msgstr ""') === 0)
					{
						# multiline + singular form
						$line = array();
						while (isset($lines[$i+1]) and !empty($lines[$i+1]))
						{
							$str = $lines[++$i];
							$line[] = substr($str, 1, strlen($str) - 2);
						}
						$msg['msgstr'] = $this->__unescape(implode(' ', $line));
					}
					elseif (preg_match('/msgstr\[(\d+)\] ""/', $line, $match))
					{
						# multiline + plural form
						$line = array();
						while (isset($lines[$i+1]) and !empty($lines[$i+1]))
						{
							$str = $lines[++$i];
							$line[] = substr($str, 1, strlen($str) - 2);
						}
						$msg['msgstr'][$match[1]] = $this->__unescape(implode(' ', $line));
					}
					elseif (preg_match('/msgstr\[(\d+)\] /', $line, $match))
					{
						# plural form
						$line = preg_replace('/^msgstr\[(\d)+\] /', '', $line);
						$line = substr($line, 1, strlen($line) - 2);
						$msg['msgstr'][$match[1]] = $this->__unescape($line);
					}
					else
					{
						# singular form
						$line = preg_replace('/^msgstr /',  '', $line);
						$line = substr($line, 1, strlen($line) - 2);
						$msg['msgstr'] = $this->__unescape($line);
					}
				}
			}
			
			if (!empty($msg['msgid']))
			{
				$id = sha1($msg['msgid']);
				$this->strings[$id] = $msg;
			}
		}
	}
	
	/**
	 * Sets one or many translations.
	 *  
	 * @param $id_or_strings Mixed Hash of ids and strings or a single id.
	 * @param $msgstr Mixed[optional] A single translation or many translations for a plural form. 
	 */
	function set($id_or_strings, $msgstr=null)
	{
		if (is_array($id_or_strings))
		{
			foreach($id_or_strings as $_msgid => $_msgstr) {
				$this->set($_msgid, $_msgstr);
			}
		}
		else
		{
			if (is_array($msgstr))
			{
				foreach($msgstr as $id => $msg) {
					$msgstr[$id] = str_replace(CR, '', $msg);	
				}
			}
			else {
				$msgstr = str_replace(CR, '', $msgstr);
			}
			$this->strings[$id_or_strings]['msgstr'] = $msgstr;
		}
	}
	
	/**
	 * Marks one or many lines as fuzzy.
	 * 
	 * @param $id_or_strings Mixed Hash of ids and strings or a single id.
	 * @param $fuzzy Boolean[optional] True if line is fuzzy.
	 */
	function set_fuzzy($id_or_strings, $fuzzy=null)
	{
		if (is_array($id_or_strings))
		{
			# remove fuzzy
			foreach(array_keys($this->strings) as $id)
			{
				$this->set_fuzzy($id, false);
			}
			
			# set fuzzy
			foreach($id_or_strings as $_msgid => $_fuzzy)
			{
				$this->set_fuzzy($_msgid, $_fuzzy);
			}
		}
		else
		{
			$this->strings[$id_or_strings]['fuzzy'] = $fuzzy;
			if ($fuzzy)
			{
				# set fuzzy
				if (!in_array('#, fuzzy', $this->strings[$id_or_strings]['#'])) {
					$this->strings[$id_or_strings]['#'][] = '#, fuzzy';
				}
			}
			else
			{
				# remove fuzzy
				$key = array_search('#, fuzzy', $this->strings[$id_or_strings]['#']);
				if ($key !== false) {
					unset($this->strings[$id_or_strings]['#'][$key]);
				}
			}
		}
	}
	
	/**
	 * Returns all strings parsed from the PO file.
	 * 
	 * @return Array
	 */
	function & get_strings()
	{
		return $this->strings;
	}
	
	/**
	 * Saves the PO file.
	 * 
	 * @return Boolean
	 * @param $strings Array[Optional]
	 * @param $fuzzy Array[Optional]
	 */
	function save(array $strings=null, array $fuzzy=null)
	{
		if ($strings !== null) {
			$this->set(&$strings);
		}
		if ($fuzzy !== null) {
			$this->set_fuzzy(&$fuzzy);
		}
		/*
		echo '<pre>';
		print_r((string)$this);
		exit;
		*/
		return file_put_contents($this->filename, (string)$this);
	}
	
	/**
	 * Renders the strings in a JSON format.
	 * 
	 * \code
	 * {
	 *   "msgid": "msgstr"
	 * }
	 * \encode
	 * 
	 * @todo Handle the plural form.
	 * @todo Handle header and comments.
	 * 
	 * @return String
	 * @param $strings Array[optional]
	 * @param $fuzzy Array[optional]
	 */
	function to_json(array $strings=null, array $fuzzy=null)
	{
		if ($strings !== null)
			$this->set(&$strings);
		
		if ($fuzzy !== null)
			$this->set_fuzzy(&$fuzzy);
		
		$strings = array();
		foreach($this->strings as $msg)
		{
			if (!$msg['fuzzy'])
			{
				$strings[$msg['msgid']] = $msg['msgstr'];
			}
		}
		return json_encode($strings);
	}
	
	// Renders the PO file.
	function __toString()
	{
		ob_start();
		echo $this->header.LF.LF;
		
		foreach($this->strings as $msg)
		{
			# comments
			if (!empty($msg['#'])) {
				echo implode(LF, $msg['#']).LF;
			}
			
			# msgid: singular form
			echo (strlen($msg['msgid']) > 68) ? 'msgid ""'.LF : 'msgid ';
			echo PO_QUOTE.$this->__format($msg['msgid']).PO_QUOTE.LF;
			
			# msgid: plural form
			if (isset($msg['msgid_plural']))
			{
				echo (strlen($msg['msgid_plural']) > 68) ? 'msgid_plural ""'.LF : 'msgid_plural ';
				echo PO_QUOTE.$this->__format($msg['msgid_plural']).PO_QUOTE.LF;
			}
			
			# msgstr
			if (!is_array($msg['msgstr']))
			{
				echo (strlen($msg['msgstr']) > 68) ? 'msgstr ""'.LF : 'msgstr ';
				echo PO_QUOTE.$this->__format($msg['msgstr']).PO_QUOTE.LF;
			}
			else
			{
				# plural form (multiple translations)
				foreach($msg['msgstr'] as $i => $msgstr)
				{
					echo (strlen($msgstr) > 68) ? "msgstr[$i] \"\"".LF : "msgstr[$i] ";
					echo PO_QUOTE.$this->__format($msgstr).PO_QUOTE.LF;
				}
			}
			echo LF;
		}
		return ob_get_clean();
	}
	
	/**
	 * Replaces quotes, escapes existing ones and reformats the given string.
	 * 
	 * @return String
	 * @param $str String
	 */
	protected function __format($str)
	{
		$str    = wordwrap(str_replace(LF, ' ', $str), 68);
		$keys   = array('"', /*"'",*/ LF);
		$values = array('\"', /*"\'",*/ PO_QUOTE.LF.PO_QUOTE);
		return str_replace($keys, $values, $str);
	}
	
	/**
	 * Unescapes quotes.
	 * 
	 * @return String
	 * @param $str String
	 */
	protected function __unescape($str)
	{
		$values = array('"', /*"'"*/);
		$keys   = array('\"', /*"\'"*/);
		return str_replace($keys, $values, $str);
	}
}

?>